﻿using System;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using DeepZoomPivotConstructor.ErrorAndLoggingInterfaces;
using DeepZoomPivotConstructor.VisualLib;
using DeepZoomPivotConstructor.VisualLib.Hotspots;
using Microsoft.Office.Core;
using Microsoft.Office.Interop.PowerPoint;
using ProcessUtilities;
using RenderEngineInterfaces;

namespace PowerPointRender
{
    /// <summary>
    /// Renders a FileInfo that points to a ppt or pptx file using
    /// PowerPoint 2007's print to PDF. It then gets the render engine to
    /// further render that puppy.
    /// </summary>
    [Export(typeof(IRenderItem))]
    [RenderInfo(typeof(FileInfo))]
    public class RenderUsingPowerPoint : IRenderItem
    {
        public Task<Visual> Render(object what)
        {
            return Task<Visual>.Factory.StartNew(() => RenderPowerPointFile(what as FileInfo));
        }

        /// <summary>
        /// Render engine to render the output of the print-to-pdf
        /// from ppt.
        /// </summary>
        [Import]
        private IRenderEngine MyRender { get; set; }

        /// <summary>
        /// Where we can write error messages
        /// </summary>
        [Import]
        private IErrorLogger MyErrorLogger { get; set; }

        /// <summary>
        /// If we should be using a different temp directory than the one that is there by default... :-)
        /// </summary>
        [Import("RenderingTempDir", AllowDefault = true)]
#pragma warning disable 0649
        private DirectoryInfo RenderingTempDir;
#pragma warning restore 0649

        /// <summary>
        /// Some layout services.
        /// </summary>
        [Import]
        private ILayoutHelper LayMeOut { get; set; }

        /// <summary>
        /// Do actual rendering of the file, perhaps on a background thread.
        /// </summary>
        /// <param name="fileInfo"></param>
        /// <returns></returns>
        private Visual RenderPowerPointFile(FileInfo talkInfo)
        {
            string name = talkInfo.Name.Substring(0, talkInfo.Name.IndexOf("."));
            string tempPath = Path.GetTempPath();
            if (RenderingTempDir != null)
            {
                tempPath = RenderingTempDir.FullName;
            }
            FileInfo pdfFile = new FileInfo(tempPath + Path.DirectorySeparatorChar + Path.GetRandomFileName() + ".pdf");
            lock (this)
            {
                Application powerApp = null;
                Presentation presentation = null;

                if (pdfFile.Exists)
                {
                    pdfFile.Delete();
                }
                int numberRetries = 0;
                while (numberRetries < 10)
                {
                    try
                    {
                        var pg = new ProcessGong("POWERPNT", 1000 * 20);
                        pg.Watcher = new ProcessGong.WatchForSingleFileChange(pdfFile);

                        powerApp = new Application();
                        //powerApp.Visible = MsoTriState.msoFalse;
                        presentation = powerApp.Presentations.Open(talkInfo.FullName, MsoTriState.msoTrue, MsoTriState.msoFalse, MsoTriState.msoFalse);

                        MyErrorLogger.LogMessage("RenderUsingPowerPoint", "Rendering " + talkInfo.Name + " - try #" + numberRetries.ToString());

                        pg.StartWait();
                        presentation.ExportAsFixedFormat(pdfFile.FullName,
                            PpFixedFormatType.ppFixedFormatTypePDF,
                            PpFixedFormatIntent.ppFixedFormatIntentPrint,
                            MsoTriState.msoFalse,
                            PpPrintHandoutOrder.ppPrintHandoutHorizontalFirst,
                            PpPrintOutputType.ppPrintOutputSlides,
                            MsoTriState.msoTrue,
                            null,
                            PpPrintRangeType.ppPrintAll,
                            "Test",
                            false,
                            false,
                            true,
                            true,
                            false,
                            System.Reflection.Missing.Value);
                        pg.StopWait();

                        MyErrorLogger.LogMessage("RenderUsingPowerPoint", "Rendering done " + talkInfo.Name);
                        numberRetries = 100;

                        ///
                        /// Shut down power point!
                        /// 

                        presentation.Close();
                        powerApp.Quit();
                    }
                    catch (COMException e)
                    {
                        MyErrorLogger.LogMessage("RenderUsingPowerPoint", "Error rendering " + talkInfo.Name, e);
                        try
                        {
                            if (presentation != null)
                            {
                                presentation.Close();
                            }
                            if (powerApp != null)
                            {
                                powerApp.Quit();
                            }
                        }
                        catch (Exception closeError)
                        {
                            MyErrorLogger.LogMessage("RenderUsingPowerPoint", "Error trying to close PowerPoint!", closeError);
                        }

                        Thread.Sleep(1000);

                        numberRetries += 1;
                    }
                }
            }

            //
            // If nothing happened - which ppt seems to do every now and then (lots of corrupt ppt on the web!), then we just get out of here.
            //

            pdfFile.Refresh();
            if (!pdfFile.Exists)
            {
                MyErrorLogger.LogMessage("RenderUsingPowerPoint", string.Format("Failed to print to PDF '{0}'", talkInfo.FullName));
                return null;
            }

            //
            // Look for sub-renderings
            //

            var subRenderings = ExtractSubImages.FindAllPossibleRenderRequests(talkInfo, MyErrorLogger);
            var subRenderInfo = (from sr in subRenderings.renderings
                                 select new
                                 {
                                     RenderedList = sr.url.Select(u => MyRender.RenderToFuture(u, requestComposableUnit: true)).ToArray(),
                                     RenderInfo = sr
                                 }).ToArray();

            //
            // Now, render the actual PDF file
            //

            var v = MyRender.Render(pdfFile);
            if (v.Info != null)
            {
                v.Info.Title = name;
            }
            else
            {
                v.Info = new Visual.VisualInfo() { Title = name };
            }

            //
            // Next, for each sub-rendering we need to make sure that we place it correctly on each slide. Add a title as well so
            // user can tell what they are looking at.
            //

            const double TextHeight = 0.5;
            const double TitleImageWidthScale = 10.0;
            var vslides = v.GetHierarchicalEnumerator(vs => vs is VisualSingleImage).Where(vs => vs is VisualSingleImage).Cast<VisualSingleImage>().ToArray();
            foreach (var sr in subRenderInfo)
            {
                Task.WaitAll(sr.RenderedList.Where(u => u != null).ToArray());
                var resultList = sr.RenderedList.Zip(sr.RenderInfo.url, (res, u) => Tuple.Create(res, u)).Where(sv => sv.Item1 != null && sv.Item1.Result != null).ToArray();
                if (resultList.Length > 0)
                {
                    //
                    // Put all the sub-talks or whatever into a title slide and visual holder ready for placement on the
                    // "final" canvas.
                    //

                    foreach (var r in resultList)
                    {
                        r.Item1.Result.ScaleToMaxSize(TitleImageWidthScale);
                    }
                    var titledVisuals = from rendered in resultList
                                        select new VisualTitleAndBackground(rendered.Item1.Result) {
                                            {TextHeight, ExtractTitleSafely(rendered.Item1.Result.Info, rendered.Item2), ExtractLinkSafely(rendered.Item2)}
                                        };

                    //
                    // Find the "slide" that we are to overlay. We have some hard-coding of understanding here: we
                    // we are assuming that the single visual is our slide number!
                    //

                    if (sr.RenderInfo.slideNumber > vslides.Length)
                    {
                        MyErrorLogger.LogMessage("RenderUsingPowerPoint", string.Format("Sub render image needs to be on slide {0}, but there are only a total of {1} slides in this talk.", sr.RenderInfo.slideNumber, vslides.Length));
                    }
                    else
                    {
                        var theSlide = vslides[sr.RenderInfo.slideNumber - 1];
                        v = PlaceOnPage(titledVisuals.ToArray(), sr.RenderInfo, theSlide, subRenderings, v);
                    }

                }
                else
                {
                    if (sr.RenderInfo.url == null)
                    {
                        MyErrorLogger.LogMessage("RenderUsingPowerPoint", string.Format("Internal error - tried to render null internal talks for '{0}'.", talkInfo.FullName));
                    }
                    else
                    {
                        foreach (var u in sr.RenderInfo.url)
                        {
                            if (u == null)
                            {
                                MyErrorLogger.LogMessage("RenderUsingPowerPoint", string.Format("Unable to render the sub-url in a powerponit talk 'null'.", u));
                            }
                            else
                            {
                                MyErrorLogger.LogMessage("RenderUsingPowerPoint", string.Format("Unable to render the sub-url in a powerponit talk '{0}'.", u));
                            }
                        }
                    }
                }
            }

            return v;

        }

        /// <summary>
        /// Try to safely create a uri that can be used as a link. If we can't, then blow it off.
        /// </summary>
        /// <param name="uriString"></param>
        /// <returns></returns>
        private HotSpotActionBase ExtractLinkSafely(string uriString)
        {
            try
            {
                return new HSActionLink(new Uri(uriString));
            }
            catch
            {
            }
            return null;
        }

        /// <summary>
        /// Extract the title if the info object is any good!
        /// </summary>
        /// <param name="visualInfo"></param>
        /// <returns></returns>
        private string ExtractTitleSafely(Visual.VisualInfo visualInfo, string alternate)
        {
            if (visualInfo != null)
                return visualInfo.Title;

            return alternate;
        }

        /// <summary>
        /// We do the dirty work of actually placing the sub-visuals on the master slide.
        /// - If the master slide isn't already a visual holder, we make it one (that would be weird)
        /// - We layout all the sub-visuals
        /// - we rescale them to fit
        /// - We add them to the master container, after positioning and rescaling them.
        /// </summary>
        /// <param name="visual"></param>
        /// <param name="subRenderInfo"></param>
        /// <param name="subRenderings"></param>
        /// <param name="v"></param>
        /// <remarks>
        /// We are relying here on teh way the PDF render works. If there is some change to that, it may break this. I'm not sure exactly
        /// how to fix that here, but.. one way to fix it would be to add a page number to the page title - then we coudl do a search. :-)
        /// </remarks>
        private Visual PlaceOnPage(Visual[] subVisualList, ExtractSubImages.SlideSubInfo.SubRenderInfo subRenderInfo, Visual theSlide, ExtractSubImages.SlideSubInfo subRenderings, Visual masterVisual)
        {
            var mContainer = masterVisual as VisualHolder;
            if (mContainer == null)
            {
                mContainer = new VisualHolder();
                mContainer.AddImage(masterVisual);
            }

            //
            // If there is more than one sub-visual here, then we need to organize them.
            //

            Visual subVisual = subVisualList[0];
            if (subVisualList.Length > 0)
            {
                subVisual = LayMeOut.LayoutIn2By3Rows(subVisualList);
            }
            subVisual.IncZIndex(mContainer.ZIndex + 1);

            //
            // Now, we are basically ready. Calculate the location and widths for teh final image insertion.
            //

            var widthPercent = subRenderInfo.width / (double)subRenderings.slideSizeX;
            var heightPercent = subRenderInfo.height / (double)subRenderings.slideSizeY;

            var widthOnSlide = widthPercent * theSlide.Width;
            var heightOnSlide = heightPercent * theSlide.Height;

            var xOffSetPercent = subRenderInfo.posX / (double)subRenderings.slideSizeX;
            var yOffSetPercent = subRenderInfo.posY / (double)subRenderings.slideSizeY;

            var xOnSlide = theSlide.XPosition + xOffSetPercent * theSlide.Width;
            var yOnSlide = theSlide.YPosition + yOffSetPercent * theSlide.Height;

            //
            // Rescale the image and insert it
            //

            subVisual.ScaleToMaxSize(widthOnSlide);
            if (subVisual.Height > heightOnSlide)
            {
                subVisual.ScaleToMaxSize(heightOnSlide);
            }

            subVisual.XPosition = xOnSlide;
            subVisual.YPosition = yOnSlide;

            mContainer.AddImage(subVisual);

            return mContainer;
        }

        /// <summary>
        /// If it is a ptt file, then we are a pretty good choice for doing the
        /// rendering of it!
        /// </summary>
        /// <param name="what"></param>
        /// <returns></returns>
        public RenderPluginPriority RenderPriority(object what)
        {
            FileInfo f = what as FileInfo;
            f.Refresh();
            if (f.Exists)
            {
                string ext = f.Extension.ToLower();
                if (ext == ".ppt" || ext == ".pptx")
                {
                    return RenderPluginPriority.RPPGoodChoice;
                }
            }

            return RenderPluginPriority.RPPCantDoIt;
        }
    }
}
